Sprint — Docker + Nginx for MCP & Copilot stack
Goal: Containerize dwd-mcp-fastmcp and dwd-copilot-server on msi-laptop-3, wire them to existing dwd-api-fastapi, expose via Nginx on port 80 (443 later), and verify server-to-server URLs use internal paths (not the public LAN IP).
Status: Phase 1 complete (local builds verified). Phase 2+ pending on server.
Repos:
| Repo | Role | Tech |
|---|---|---|
dwd-api-fastapi | Forecast API + MAF /agent | Python / Gunicorn |
dwd-mcp-fastmcp | MCP tools for agent | Python / Gunicorn / FastMCP |
dwd-copilot-server | CopilotKit runtime → proxies to /agent | Node 22 / Next.js |
Architecture (target)
Browser (localhost:3000 or LAN)
│
▼
Nginx :443 (www.hydrotrek.org → 74.83.141.228)
├── /gqc/api/fastapi/ → 127.0.0.1:8001 → dwd-api-fastapi :8080
├── /gqc/api/mcp → 127.0.0.1:8002 → dwd-mcp-fastmcp :8081 (upstream /mcp)
└── /gqc/api/copilotkit → 127.0.0.1:8003 → dwd-copilot-server :8082
/ and non-client paths → 404
Docker network (server-to-server — NOT via LAN IP):
dwd-copilot-server ──AGUI_AGENT_URL──► dwd-api-fastapi:8080/agent
dwd-api-fastapi ──MCP_SERVER_URL──► dwd-mcp-fastmcp:8081/mcp
dwd-mcp-fastmcp ──DWD_API_BASE_URL──► ASP.NET (external; see below)
Port allocation
Align container PORT with your request (8080 → 8081 → 8082). Host bindings stay on 127.0.0.1 only (existing convention).
| Service | Container port | Host bind (127.0.0.1) | Nginx path(s) |
|---|---|---|---|
dwd-api-fastapi | 8080 | 8001 | /gqc/api/fastapi/ |
dwd-mcp-fastmcp | 8081 | 8002 | /gqc/api/mcp (→ upstream /mcp) |
dwd-copilot-server | 8082 | 8003 | /gqc/api/copilotkit |
Record in README.md deployed APIs table when live.
URL / cross-reference matrix
Server-to-server (use Docker Compose service names)
Do not use http://74.83.141.228/... or the public hostname between containers (hairpin, unnecessary exposure).
| From | Env var | Target value (container network) |
|---|---|---|
dwd-copilot-server | AGUI_AGENT_URL | http://dwd-api-fastapi:8080/agent |
dwd-api-fastapi | MCP_SERVER_URL | http://dwd-mcp-fastmcp:8081/mcp |
dwd-api-fastapi | MCP_SERVER_NAME | HydroTrek MCP (unchanged) |
Use a shared Compose network (e.g. dwd-stack) so hostnames resolve.
Note: 127.0.0.1 inside a container is that container only. For server-to-server, use service names above, not localhost.
Nginx → upstream (host localhost)
Nginx proxy_pass | Upstream |
|---|---|
| FastAPI routes | http://127.0.0.1:8001 |
/mcp | http://127.0.0.1:8002 |
/api/copilotkit | http://127.0.0.1:8003 |
Browser / Vite frontend (developer workstation)
| Client calls | URL (LAN test) |
|---|---|
| Forecast / REST | https://www.hydrotrek.org/gqc/api/fastapi/graph-data |
| CopilotKit | https://www.hydrotrek.org/gqc/api/copilotkit |
| Agent (direct; usually not browser) | Copilot server proxies to FastAPI |
Frontend env (e.g. VITE_*) should point at https://www.hydrotrek.org/gqc/api/..., not raw container ports or 74.83.141.228 (cert hostname mismatch on IP).
ASP.NET dwd-api-aspnet (external to this stack)
Runs on developer Windows PC (e.g. :7033). Server containers cannot use localhost:7033 — use Windows LAN IP (see 13-cors-and-browser-api.md).
| Service | Env var | Dev value example |
|---|---|---|
dwd-api-fastapi | DWD_API_BASE_URL | https://192.168.0.XX:7033/api/ |
dwd-mcp-fastmcp | DWD_API_BASE_URL | https://192.168.0.XX:7033 (no /api/ suffix) |
Phase 0 — Prerequisites
-
dwd-api-fastapirunning:curl http://127.0.0.1:8001/health - Nginx LAN config working for FastAPI (11-nginx-lan-setup.md)
- Docker + compose on server (08-docker-install-and-users.md)
- Git deploy keys for all three repos (07-code-management-and-git.md)
-
OPENAI_API_KEYin/etc/dwd-api-fastapi/dwd-api-fastapi.env(required for/agent) - Clones under
/opt/apis/for all three repos - Docker files committed & pushed for
dwd-api-fastapi(already started); same pattern for two new repos
Phase 1 — Docker files in each repo
1A — dwd-mcp-fastmcp
-
Dockerfile(Python 3.11-slim, gunicorn +uvicorn.workers.UvicornWorker,PORT=8081) -
.dockerignore -
GET /healthverified in image - Local build test:
docker build -t dwd-mcp-fastmcp . - Compose / network config → hydrotrek-dwd-suite/docker/ (not in submodule)
Reference: templates/docker/dwd-mcp-fastmcp/ (Dockerfile only)
1B — dwd-copilot-server
-
Dockerfile(Node 22.22.2,npm run build:standalone,PORT=8082) -
.dockerignore - Production start command:
node server.jsfrom standalone output - Local build test (Docker Desktop on workstation)
- Compose / network config → hydrotrek-dwd-suite/docker/ (not in submodule)
Reference: repos/dwd-copilot-server/README.md, templates/docker/dwd-copilot-server/ (Dockerfile only)
1C — Stack compose in hydrotrek-dwd-suite
- Single stack compose in
repos/hydrotrek-dwd-suite/docker/ -
docker-compose.yml(local dev — submodule build contexts) -
docker-compose.prod.yml→/etc/dwd-stack/docker-compose.ymlon server -
dwd-mcp-fastmcpadded as submodule in suite - Cross-service env wired in compose (
MCP_SERVER_URL,AGUI_AGENT_URL) - Nginx template: [docker/nginx/dwd-gqc-lan.conf]
Decision: Cross-repo Docker orchestration lives in hydrotrek-dwd-suite only. Submodule repos keep Dockerfile + .dockerignore for builds. Server production compose path: /etc/dwd-stack/docker-compose.yml.
Phase 2 — Server layout (/etc/)
| Path | Purpose |
|---|---|
/etc/dwd-stack/docker-compose.yml | from hydrotrek-dwd-suite/docker/docker-compose.prod.yml |
/etc/dwd-api-fastapi/dwd-api-fastapi.env | OPENAI_API_KEY, DWD_API_BASE_URL, … |
/etc/dwd-mcp-fastmcp/dwd-mcp-fastmcp.env | DWD_API_BASE_URL, … |
/etc/dwd-copilot-server/dwd-copilot-server.env | optional CORS extras |
/etc/systemd/system/dwd-stack.service | optional boot unit for full stack |
Env templates
- Create dirs (
750,root:apisvc) - Env files mode
640 - Copy stack compose to
/etc/dwd-stack/docker-compose.yml -
MCP_SERVER_URLandAGUI_AGENT_URLset in compose (not env files)
Phase 3 — Deploy containers
Order: stack compose up (MCP + FastAPI + Copilot together), or bring up MCP first then docker compose up -d for the rest.
cd /etc/dwd-stack
sudo docker compose up -d --build
curl -s http://127.0.0.1:8002/health # mcp
curl -s http://127.0.0.1:8001/health # fastapi + mcp_connected
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8003/ # copilot
3A — MCP
- Container healthy
-
127.0.0.1:8002only (not0.0.0.0:8002)
3B — FastAPI (with MCP)
-
MCP_SERVER_URLin stack compose (Docker DNS URL) -
curl -s http://127.0.0.1:8001/health→mcp_configured: true,mcp_connected: true
3C — Copilot server
-
AGUI_AGENT_URLin stack compose - Build (may take longer — Next standalone)
-
curl -s http://127.0.0.1:8003/or hit/api/copilotkitOPTIONS/POST smoke test
Phase 4 — Nginx (single LAN IP, port 80)
Replace or extend templates/nginx/dwd-api-fastapi-lan.conf with path-based routing:
| Location | Upstream |
|---|---|
/gqc/api/fastapi/ | 127.0.0.1:8001 |
/gqc/api/copilotkit | 127.0.0.1:8003 |
/gqc/api/mcp | 127.0.0.1:8002 |
/ | 404 |
- WebSocket / long timeout headers where needed (Copilot + agent)
-
proxy_http_version 1.1for/mcpand/api/copilotkit -
sudo nginx -t && sudo systemctl reload nginx - Remove or narrow
default_serverif only one combined config
HTTPS (later): duplicate blocks on 443 with same location paths.
Template: repos/hydrotrek-dwd-suite/docker/nginx/dwd-gqc-lan.conf (mirror: templates/nginx/dwd-gqc-lan.conf)
Phase 5 — Integration tests
Run from server first, then browser.
Health chain
curl -s http://127.0.0.1/health # via nginx → fastapi
curl -s http://127.0.0.1:8002/health # mcp direct
curl -s http://127.0.0.1:8001/health | jq .agent # mcp_connected
- FastAPI
/health→agent.enabled: true(withOPENAI_API_KEY) - FastAPI
/health→mcp_configured/mcp_connectedtrue - MCP
/healthreturnsdwd_api_base_url
Through Nginx (LAN IP)
curl -s https://www.hydrotrek.org/gqc/api/fastapi/health
curl -s -X OPTIONS https://www.hydrotrek.org/gqc/api/copilotkit -H "Origin: http://localhost:3000"
curl -s -o /dev/null -w "%{http_code}\n" https://www.hydrotrek.org/
- CORS on copilot route (
localhost:3000in allow list; no 301 on OPTIONS — see 15-api-reverse-proxy-base-paths-todo.md) - CORS on fastapi
/graph-data(13-cors-and-browser-api.md)
End-to-end (manual)
- Vite
:3000→https://www.hydrotrek.org/gqc/api/fastapi/graph-data - Vite CopilotKit →
https://www.hydrotrek.org/gqc/api/copilotkit→ agent responds - Agent tool call hits MCP (check MCP container logs for tool execution)
- No server logs showing requests to
74.83.141.228between containers (only Docker DNS)
Log commands
sudo docker compose -f /etc/dwd-stack/docker-compose.yml logs -f --tail=30
sudo docker compose -f /etc/dwd-stack/docker-compose.yml logs -f dwd-mcp-fastmcp --tail=30
sudo tail -f /var/log/nginx/error.log
Phase 6 — Documentation & git
- Update README.md deployed APIs table (ports 8002, 8003)
- Add
docs/15-deploy-dwd-stack.md(copy-paste runbook, post-sprint) - Add 15-api-reverse-proxy-base-paths-todo.md (ROOT_PATH, Swagger, Copilot preflight)
- Push Docker files: submodule
Dockerfiles + hydrotrek-dwd-suite/docker/ stack - Record env var final values (redact secrets)
Phase 7 — Deferred (server room)
- Subdomain routing instead of path-based /
default_server - SSL on 443
- UFW: 80/443 on Ethernet only (09-wifi-and-ssh-network-setup.md)
- Production CORS origins (hydrotrek.com, etc.)
Risks / notes
| Topic | Note |
|---|---|
| Next.js image size | Copilot standalone build; multi-stage Dockerfile |
| MCP + FastAPI startup order | FastAPI discovers MCP at lifespan startup — restart FastAPI after MCP is up |
localhost vs service names | Browser uses server IP; containers use Compose DNS |
| ASP.NET on Windows | Still needs LAN IP from server; not part of this stack |
| OpenAI key | Required for /agent; without it copilot connects but agent disabled |
| Single vs split compose | Shared network required for http://dwd-api-fastapi:8080 URLs |
Sprint checklist (quick)
[ ] Phase 0 — prerequisites
[x] Phase 1 — Dockerfiles in both repos (+ push)
[ ] Phase 2 — /etc/ env + compose
[ ] Phase 3 — MCP up → FastAPI recreate → Copilot up
[ ] Phase 4 — Nginx path routing
[ ] Phase 5 — health + browser + MCP tool smoke test
[ ] Phase 6 — docs + README ports